// *********************************************************
//      Copyright (c) Kosta Gaitanis
//      All rights reserved
//
//      k.gaitanis@gmail.com
//
// *********************************************************

#ifndef CAMERA_H
#define CAMERA_H

#include <QtOpenGL/QtOpenGL>
#include "Matrix.h"
#include "Vector.h"

/**
 * @brief The Camera class implements a camera in OpenGL
 * \author Kosta Gaitanis
 */
class Camera : public QObject
{
    Q_OBJECT
public:
    /**
     * Constructor
     */
    Camera(QObject *parent = 0);

    /**
     * Destructor
     */
    ~Camera();

    /**
     * @brief Initializes the projection matrix of OpenGL.
     * Uses (Field of view, aspect ratio and near/far limits.
     * This function is called once at initialization and every time
     * the camera settings are modified.
     */
    void initializeGL();

    /**
     * @brief Initializes the ModelView matrix of OpenGL.
     * Uses Camera position, target position and up direction.
     * This is called for every frame before anything else is drawn.
     */
    void paint();

    /**
     * @brief Moves the camera closer/further to the target
     * @param zoomFactor > 1.0 will move the camera further
     */
    void zoom(float zoomFactor);

    /**
     * @brief Rotates the camera around the target, keeping the same
     *        distance to the target.
     * @param theta is the angle to rotate in a plane that is vertical to the camera's
     *        up direction
     * @param phi is the angle to rotate in a plane parallel with the camera's up direction.
     */
    void rotateCameraAroundTarget(float theta, float phi);

    /**
     * @brief Rotates the camera around the direction where the camera is looking, keeping
     *        the position of the camera unchanged.
     * @param theta angle to rotate the camera/
     */
    void rotateCamera(float theta);

    /**
     * @brief sets the position of the camera.
     * @param position The new position of the camera.
     */
    void setCameraPosition(const Vector& position);

    /**
     * @brief sets the position of the target (where the camera looks).
     * @param position The new position of the target.
     */
    void setTargetPosition(const Vector& position);

    /**
     * @brief sets the up direction of the camera.
     * @param position The new up direction of the camera.
     */
    void setUpDirection(const Vector& up);

    /**
     * @brief Same as calling setCameraPosition(Vector(x,y,z))
     * @param {x,y,z} position of the camera.
     */
    void setCameraPosition(double x, double y, double z);

    /**
     * @brief Same as calling setTargetPosition(Vector(x,y,z))
     * @param {x,y,z} position of the target.
     */
    void setTargetPosition(double x, double y, double z);

    /**
     * @brief Same as calling setUpDirection(Vector(x,y,z))
     * @param {x,y,z} up direction of the camera.
     */
    void setUpDirection(double x, double y, double z);

    /**
     * @brief sets the VERTICAL field of view of the camera in degrees.
     * @param degrees
     */
    void setFieldOfView(double degrees);

    /**
     * @brief sets the Aspect Ratio of the camera
     * @param ratio  = width / height
     */
    void setAspectRatio(double ratio);

    /**
     * @brief sets the near limit of the camera.
     * @param near The near limit.
     */
    void setNearLimit(double nearLimit);

    /**
     * @brief sets the far limit of the camera.
     * @param near The far limit.
     */
    void setFarLimit(double farLimit);

    /**
     * @return The vertical field of view of the camera.
     */
    double getFieldOfView() const;

    /**
     * @return The aspect ratio (w/h) of the camera.
     */
    double getAspectRatio() const;

    /**
     * @return The near limit of the camera.
     */
    double getNearLimit() const;

    /**
     * @return The far limit of the camera.
     */
    double getFarLimit() const;

    /**
     * @return The position of the camera.
     */
    Vector getCameraPosition() const;

    /**
     * @return The position of the target.
     */
    Vector getTargetPosition() const;

    /**
     * @return The up direction of the camera.
     */
    Vector getUpDirection() const;

    /**
     * @return The position matrix of the camera.
     * This is the initial matrix we load in the ModelView matrix.
     * This function is used for testing the correctness of the calculated matrix.
     */
    Matrix getCameraMatrix() const;

    /**
     * @return The size of the frustum of the camera.
     * This is used to verify the correctness of the calculated frustum.
     */
    QSizeF getFrustumSize() const;

signals:
    /**
     * @brief This signal is emitted every time the camera parameters are modified.
     *        Used to synchronize the camera widget.
     */
    void changed();

public slots:
    /**
     * @brief Called every time the window is resized. Adapts the viewport settings.
     * @param {width,height} the size of the window (in pixels)
     */
    void onWindowResized(int width, int height);

private:
    /**
     * @brief Called when camera position, target position or up direction have
     * been changed. Recalculates the model view matrix and stores it into
     * m_position.
     */
    void calculateCameraMatrix();

    /**
     * @brief Stores the frustum size for verification reasons.
     * @param {w, h} The width/height of the frustum.
     */
    void setFrustumSize(GLdouble w, GLdouble h);

    /**
     * @brief stores the camera Matrix for verification reasons.
     * @param cameraMatrix
     */
    void setCameraMatrix(Matrix cameraMatrix);

    Vector m_cameraPosition;                 /// > The camera position.
    Vector m_targetPosition;                 /// > The target position.
    Vector m_upDirection;                    /// > The camera up direction.
    Matrix m_cameraMatrix;                          /// > The stored modelView matrix.
    double m_fov, m_aspectRatio, m_near, m_far;     /// > Field of View, aspect ration, near/far limits.

    // for verification (see tests)
    GLdouble m_frustumWidth, m_frustumHeight;        /// > The stored frustum size.
};

#endif // CAMERA_H
